import { getElementByXPath, getElementByLocator } from './dom_utils';

/*
 * Basic tool function
 */

let extend = function () {
    let args = Array.from(arguments);
    let len = args.length;

    if (len <= 0) return {};
    if (len === 1) return args[0];

    let head = args[0];
    let rest = args.slice(1);

    return rest.reduce(function (prev, cur) {
        for (let i = 0, keys = Object.keys(cur), len = keys.length; i < len; i++) {
            prev[keys[i]] = cur[keys[i]];
        }

        return prev;
    }, head);
};

let isArray = Array.isArray;

let id = function (x) {
    return x;
};

let trim = function (str) {
    return str.replace(/^\s*|\s*$/g, '');
};

let flatten = function (list) {
    return [].concat.apply([], list);
};

let sum = function () {
    let list = Array.from(arguments);
    return list.reduce(function (prev, cur) {
        return prev + cur;
    }, 0);
};

let last = function (list) {
    return list[list.length - 1];
};

let or = function (list) {
    return (list || []).reduce(function (prev, cur) {
        return prev || cur;
    }, false);
};

let and = function (list) {
    return (list || []).reduce(function (prev, cur) {
        return prev && cur;
    }, true);
};

let zipWith = function (fn) {
    if (arguments.length < 3) return null;

    let list = Array.from(arguments).slice(1);
    let len = list.reduce(function (min, cur) {
        return cur.length < min ? cur.length : min;
    }, Infinity);
    let ret = [];

    for (let i = 0; i < len; i++) {
        ret.push(
            fn.apply(
                null,
                list.map(function (item) {
                    return item[i];
                })
            )
        );
    }

    return ret;
};

let intersect = function () {
    let list = Array.from(arguments);
    let len = Math.max.apply(
        null,
        list.map(function (item) {
            return item.length;
        })
    );
    let result = [];

    for (let i = 0; i < len; i++) {
        let val = list[0][i];
        let no = list.filter(function (item) {
            return item[i] !== val;
        });

        if (no && no.length) break;

        result.push(val);
    }

    return result;
};

let deepEqual = function (a, b) {
    if (isArray(a) && isArray(b)) {
        return a.length === b.length && and(zipWith(deepEqual, a, b));
    }

    if (typeof a === 'object' && typeof b === 'object') {
        // TODO
        return false;
    }

    return a === b;
};

/*
 * Dom helper function
 */

let pixel = function (num) {
    if ((num + '').indexOf('px') !== -1) return num;
    return (num || 0) + 'px';
};

let getStyle = function (dom, styleName) {
    if (!dom) throw new Error('getStyle: dom does not exist');
    return getComputedStyle(dom)[styleName];
};

let setStyle = function (dom, style) {
    if (!dom) throw new Error('setStyle: dom does not exist');

    for (let i = 0, keys = Object.keys(style), len = keys.length; i < len; i++) {
        dom.style[keys[i]] = style[keys[i]];
    }

    return dom;
};

let cssSum = function (dom, list) {
    let isInline = getStyle(dom, 'display') === 'inline';

    return list.reduce(function (prev, cur) {
        let val = isInline && ['width', 'height'].indexOf(cur) !== -1 ? dom.getClientRects()[0][cur] : getStyle(dom, cur);

        return prev + parseInt(val || '0', 10);
    }, 0);
};

let offset = function (dom, noPx) {
    if (!dom) return { left: 0, top: 0 };

    let rect = dom.getBoundingClientRect();
    let fn = noPx ? id : pixel;

    return {
        left: fn(rect.left + window.scrollX),
        top: fn(rect.top + window.scrollY),
    };
};

let rect = function (dom, noPx) {
    let pos = offset(dom, noPx);
    let isInline = getStyle(dom, 'display') === 'inline';
    let w = isInline ? dom.getClientRects()[0]['width'] : getStyle(dom, 'width');
    let h = isInline ? dom.getClientRects()[0]['height'] : getStyle(dom, 'height');
    let fn = noPx ? id : pixel;

    return extend({ width: fn(w), height: fn(h) }, pos);
};

// Reference: http://ryanve.com/lab/dimensions/
let clientWidth = function (document) {
    return document.documentElement.clientWidth;
};

let clientHeight = function (document) {
    return document.documentElement.clientHeight;
};

let removeChildren = function (dom, predicate) {
    let pred =
        predicate ||
        function () {
            return true;
        };
    let children = dom.childNodes;

    for (let i = children.length - 1; i >= 0; i--) {
        if (pred(children[i])) {
            dom.removeChild(children[i]);
        }
    }
};

let inDom = function ($outer, $el) {
    if (!$el) return false;
    if ($outer === $el) return true;
    return inDom($outer, $el.parentNode);
};

let inDomList = function (list, $el) {
    return or(
        list.map(function ($outer) {
            return inDom($outer, $el);
        })
    );
};

let parentWithTag = function (tag, $el) {
    let lowerTag = tag.toLowerCase();
    let $dom = $el;

    while ($dom) {
        if ($dom.tagName.toLowerCase() === lowerTag) {
            return $dom;
        }

        $dom = $dom.parentNode;
    }

    return null;
};

let parentWithClass = function (className, $el) {
    let $dom = $el;

    while ($dom) {
        // Note: In Firefox, HTML Document object doesn't have `classList` property
        if ($dom.classList !== undefined && $dom.classList.contains(className)) {
            return $dom;
        }

        $dom = $dom.parentNode;
    }

    return null;
};

let selector = function (dom) {
    if (dom.nodeType !== 1) return '';
    if (dom.tagName === 'BODY') return 'body';
    if (dom.id) return '#' + dom.id;

    let classes = (dom.getAttribute('class') || '').split(/\s+/g).filter(function (item) {
        return item && item.length;
    });

    let children = Array.from(dom.parentNode.childNodes).filter(function ($el) {
        return $el.nodeType === 1;
    });

    let sameTag = children.filter(function ($el) {
        return $el.tagName === dom.tagName;
    });

    let sameClass = children.filter(function ($el) {
        let cs = ($el.getAttribute('class') || '').split(/\s+/g);

        return and(
            classes.map(function (c) {
                return cs.indexOf(c) !== -1;
            })
        );
    });

    let extra = '';

    if (sameTag.length === 1) {
        extra = '';
    } else if (classes.length && sameClass.length === 1) {
        extra = '.' + classes.join('.');
    } else {
        extra =
            ':nth-child(' +
            (1 +
                children.findIndex(function (item) {
                    return item === dom;
                })) +
            ')';
    }

    let me = dom.tagName.toLowerCase() + extra;

    // Note: browser will add an extra 'tbody' when tr directly in table, which will cause an wrong selector,
    // so the hack is to remove all tbody here
    let ret = selector(dom.parentNode) + ' > ' + me;
    return ret;
    // return ret.replace(/\s*>\s*tbody\s*>?/g, ' ')
};
// 同节点下的index
let getTagIndex = function (dom) {
    return Array.from(dom.parentNode.childNodes)
        .filter(function (item) {
            return item.nodeType === dom.nodeType && item.tagName === dom.tagName;
        })
        .reduce(function (prev, node, i) {
            if (prev !== null) return prev;
            return node === dom ? i + 1 : prev;
        }, null);
};
//  返回div或div[2]，带相对位置
let relativeXPath = function (dom) {
    if (!dom) return null;
    if (dom.nodeType === 3) return '@text';

    let index = getTagIndex(dom);
    let tag = dom.tagName.toLowerCase();

    return index > 1 ? tag + '[' + index + ']' : tag;
};
// 返回//*[@id="2"]/div/div/h3/a，一直向上找到有id的节点
let xpath = function (dom, cur, list) {
    let helper = function (dom, cur, list) {
        if (!dom) return null;

        if (!cur) {
            if (dom.nodeType === 3) {
                return helper(dom.parentNode);
            } else {
                return helper(dom, dom, []);
            }
        }

        if (!cur.parentNode) {
            return ['html'].concat(list);
        }

        if (cur.tagName === 'BODY') {
            return ['html', 'body'].concat(list);
        }

        if (cur.id) {
            return [`*[@id="${cur.id}"]`].concat(list);
        }

        return helper(dom, cur.parentNode, [relativeXPath(cur)].concat(list));
    };

    let parts = helper(dom, cur, list);
    let prefix = parts[0] === 'html' ? '/' : '//';
    let ret = prefix + parts.join('/');

    return ret;
};
// 返回'//div[4]/div/div/h3/a' ，一直向上找到可使用getElementByXPath找到元素
let xpathPosition = function (dom) {
    let path = '';
    let current = dom;

    try {
        while (current !== null) {
            let currentPath;

            if (current.parentNode != null) {
                currentPath = '/' + relativeXPath(current);
            } else if (current.tagName === 'BODY') {
                currentPath = 'html/body';
            } else {
                currentPath = '/' + current.nodeName.toLowerCase();
            }

            path = currentPath + path;
            let locator = '/' + path;

            if (dom === getElementByXPath(locator)) {
                return locator;
            }

            current = current.parentNode;
        }
    } catch (e) {}

    return null;
};
// 处理text中的引号
let attributeValue = function (value) {
    if (value.indexOf("'") < 0) {
        return "'" + value + "'";
    } else if (value.indexOf('"') < 0) {
        return '"' + value + '"';
    } else {
        let result = 'concat(';
        let part = '';
        let didReachEndOfValue = false;
        while (!didReachEndOfValue) {
            let apos = value.indexOf("'");
            let quot = value.indexOf('"');
            if (apos < 0) {
                result += "'" + value + "'";
                didReachEndOfValue = true;
                break;
            } else if (quot < 0) {
                result += '"' + value + '"';
                didReachEndOfValue = true;
                break;
            } else if (quot < apos) {
                part = value.substring(0, apos);
                result += "'" + part + "'";
                value = value.substring(part.length);
            } else {
                part = value.substring(0, quot);
                result += '"' + part + '"';
                value = value.substring(part.length);
            }
            result += ',';
        }
        result += ')';
        return result;
    }
};
// 属性xpath
let xpathAttr = function (dom) {
    // 当前元素属性xpath
    function attributesXPath(name, attNames, attributes) {
        let locator = '//' + name + '[';
        for (let i = 0; i < attNames.length; i++) {
            if (i > 0) {
                locator += ' and ';
            }
            let attName = attNames[i];
            locator += '@' + attName + '=' + attributeValue(attributes[attName]);
        }
        locator += ']';
        return locator;
    }

    try {
        let PREFERRED_ATTRIBUTES = ['id', 'name', 'value', 'type', 'action', 'onclick'];
        let i = 0;

        if (dom.attributes) {
            let atts = dom.attributes;
            let attsMap = {};
            for (i = 0; i < atts.length; i++) {
                let att = atts[i];
                attsMap[att.name] = att.value;
            }
            let names = [];
            // try preferred attributes
            for (i = 0; i < PREFERRED_ATTRIBUTES.length; i++) {
                let name = PREFERRED_ATTRIBUTES[i];

                if (attsMap[name] != null) {
                    names.push(name);

                    let locator = attributesXPath(dom.nodeName.toLowerCase(), names, attsMap);

                    if (dom === getElementByXPath(locator)) {
                        return locator;
                    }
                }
            }
        }
    } catch (e) {}

    return null;
};
// 没调用
let atXPath = function (xpath, document) {
    let lower = function (str) {
        return str && str.toLowerCase();
    };
    let reg = /^([a-zA-Z0-9]+)(\[(\d+)\])?$/;

    return xpath.reduce(function (prev, cur) {
        if (!prev) return prev;
        if (!prev.childNodes || !prev.childNodes.length) return null;

        let match = cur.match(reg);
        let tag = match[1];
        let index = match[3] ? parseInt(match[3], 10) : 1;
        let list = Array.from(prev.childNodes).filter(function (item) {
            return item.nodeType === 1 && lower(item.tagName) === lower(tag);
        });

        return list[index - 1];
    }, document);
};
// textContent
let domText = ($dom) => {
    let it = $dom.innerText && $dom.innerText.trim();
    let tc = $dom.textContent;
    let pos = tc.toUpperCase().indexOf(it.toUpperCase());

    return tc.substr(pos, it.length);
};

let getFirstWorkingLocator = (locators, $el) => {
    for (let i = 0, len = locators.length; i < len; i++) {
        let $match = (() => {
            try {
                return getElementByLocator(locators[i]);
            } catch (e) {
                return null;
            }
        })();

        if ($el === $match) {
            return locators[i];
        }
    }

    return null;
};

// Note: get the locator of a DOM
let getLocator = ($dom, withAllOptions) => {
    let id = $dom.getAttribute('id');
    let name = $dom.getAttribute('name');
    let isLink = $dom.tagName.toLowerCase() === 'a';
    let text = (() => {
        try {
            return domText($dom);
        } catch (e) {
            return null;
        }
    })();
    let classes = Array.from($dom.classList);
    let candidates = [];

    // link
    if (isLink && text && text.length) {
        let links = [].slice.call(document.getElementsByTagName('a'));
        let matches = links.filter(($el) => domText($el) === text);
        let index = matches.findIndex(($el) => $el === $dom);

        if (index !== -1) {
            candidates.push(index === 0 ? `linkText=${text}` : `linkText=${text}@POS=${index + 1}`);
        }
    }

    // id
    if (id && id.length) {
        candidates.push(`id=${id}`);
    }

    // name
    if (name && name.length) {
        candidates.push(`name=${name}`);
    }

    // xpath
    candidates.push('xpath=' + xpath($dom));

    let attrXPath = xpathAttr($dom);

    if (attrXPath) {
        candidates.push('xpath=' + attrXPath);
    }

    let positionXPath = xpathPosition($dom);

    if (positionXPath) {
        candidates.push('xpath=' + positionXPath);
    }

    // css
    // Try with simple css selector first. If not unqiue, use full css selector
    /**
   * Below is the old logic with a shorter css selector
   *

  let sel = null

  if (classes.length > 0) {
    sel = $dom.tagName.toLowerCase() + classes.map(c => '.' + c).join('')

    if ($dom !== document.querySelectorAll(sel)[0]) {
      sel = null
    }
  }

  if (!sel) {
    sel = selector($dom)
  }
  */
    candidates.push(`css=${selector($dom)}`);

    // Get the first one working
    let chosen = getFirstWorkingLocator(candidates, $dom);

    if (withAllOptions) {
        return {
            target: chosen,
            targetOptions: candidates,
        };
    }

    return chosen;
};

let checkIframe = (iframeWin) => {
    let key = new Date() * 1 + '' + Math.random();

    try {
        iframeWin[key] = 'asd';
        return iframeWin[key] === 'asd';
    } catch (e) {
        return false;
    }
};

// Note: get the locator for frame
let getFrameLocator = (frameWin, win) => {
    if (checkIframe(frameWin)) {
        let frameDom = frameWin.frameElement;
        let locator = getLocator(frameDom);

        if (/^id=/.test(locator) || /^name=/.test(locator)) {
            return locator;
        }
    }

    for (let i = 0, len = win.frames.length; i < len; i++) {
        if (win.frames[i] === frameWin) {
            return `index=${i}`;
        }
    }

    throw new Error('Frame locator not found');
};

/*
 * Mask related
 */

let maskFactory = function () {
    let cache = [];
    let prefix = '__mask__' + new Date() * 1 + Math.round(Math.random() * 1000) + '__';
    let uid = 1;
    let defaultStyle = {
        position: 'fixed',
        zIndex: '999',
        display: 'none',
        boxSizing: 'border-box',
        backgroundColor: 'red',
        // opacity: 0.5,
        pointerEvents: 'none',
    };

    let genMask = function (style, dom) {
        let mask = document.createElement('div');

        if (dom) {
            style = extend({}, defaultStyle, style || {}, rect(dom));
        } else {
            style = extend({}, defaultStyle, style || {});
        }

        setStyle(mask, style);
        mask.id = prefix + uid++;
        cache.push(mask);

        return mask;
    };

    let clear = function () {
        for (let i = 0, len = cache.length; i < len; i++) {
            let mask = cache[i];

            if (mask && mask.parentNode) {
                mask.parentNode.removeChild(mask);
            }
        }
    };

    return {
        gen: genMask,
        clear: clear,
    };
};
// 显示mask
let showMaskOver = function (mask, el) {
    let rect = el.getBoundingClientRect();

    setStyle(mask, {
        width: pixel(rect.width),
        height: pixel(rect.height),
        top: pixel(rect.top),
        left: pixel(rect.left),
        display: 'block',
    });
};

let isVisible = function (el) {
    if (el === window.document) return true;
    if (!el) return true;

    let style = window.getComputedStyle(el);
    if (style.display === 'none' || style.opacity === '0' || style.visibility === 'hidden') return false;

    return isVisible(el.parentNode);
};

export default {
    offset,
    setStyle,
    selector,
    xpath,
    atXPath,
    domText,
    getLocator,
    getFrameLocator,
    maskFactory,
    showMaskOver,
    inDom,
    isVisible,
    parentWithTag,
    parentWithClass,
};
